package org.chispa.api.util;

import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.chispa.api.util.Bin.Relation;

public class BinningNumber extends Number{
	
	private Number nativeNum;
		
	private int bucketNumber;
	
	private int bucketOrdinal;
	
	private Mac cachedMac;
	
	private SecretKeySpec cachedSpec;
	
	private List<Bin> cachedBin;
	
	private Long[] cachedIndex;
	
	private static final int MOD_MULTIPLIER=1024;
	
	private boolean onLimit;
	
	private double difference;
	
	private double low;
	
	private double high;
	
	public static class IdPair{
		
		private long own;
		private long other;
		
		public IdPair(long own,long other)
		{
			this.own=own;
			this.other=other;
		}

		public long getOwn() {
			return own;
		}

		public long getOther() {
			return other;
		}
		
		@Override
		public String toString()
		{
			return own+":"+other;
		}
		
		@Override
		public boolean equals(Object what)
		{
			if(what instanceof IdPair)
				return (((IdPair)what).getOther()==other) &&
						(((IdPair)what).getOwn()==own);
			return super.equals(what);
		}
		
		private static List<IdPair>switchPairs(List<IdPair> original)
		{
			ArrayList<IdPair> switched=new ArrayList<IdPair>();
			for(IdPair orig:original)
			{
				switched.add(new IdPair(orig.getOther(),orig.getOwn()));
			}
			return switched;
		}
	}
	
		
	public BinningNumber(Number nativeNum,Number lowerThreshold,Number upperThreshold,int bucketNumber)
	{
		this.nativeNum=nativeNum;
		high=upperThreshold.doubleValue();
		low=lowerThreshold.doubleValue();
		//We add two bucket for values less than the minimum or greater than the maximum 
		this.bucketNumber=bucketNumber+2;
		double lower=lowerThreshold.doubleValue();
		double upper=upperThreshold.doubleValue();
		double num=nativeNum.doubleValue();
		if(num<=lower)
			bucketOrdinal=0;
		else if(num>upper)
			bucketOrdinal=this.bucketNumber-1;
		else{
			difference=(upper-lower)/bucketNumber;
			double nNum=num-lower;
			bucketOrdinal=(int)(nNum/difference);
			if(nNum%difference!=0.0d)
				bucketOrdinal++;
			else
				onLimit=true;
		}	
		
	}
	
	public BinningNumber cloneFromDefault(Number numValue)
	{
		return new BinningNumber(numValue,low, high, bucketNumber-2);
	}
	
	public Number getNative()
	{
		return nativeNum;
	}

	private static final long serialVersionUID = 568528928484478369L;

	@Override
	public int intValue() {
		return nativeNum.intValue();
	}

	@Override
	public long longValue() {
		return nativeNum.longValue();
	}

	@Override
	public float floatValue() {
		return nativeNum.floatValue();
	}

	@Override
	public double doubleValue() {
		return nativeNum.doubleValue();
	}

	public int getBucketSize() {
		return bucketNumber;
	}
	
	public int getBucketOrdinal()
	{
		return bucketOrdinal;
	}

	public Long[] computeIndexTable(Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		if(mac.equals(cachedMac) && spec.equals(cachedSpec))
			return cachedIndex;
		cachedMac=mac;
		cachedSpec=spec;
		cachedIndex=new Long[bucketNumber];
		try {
			mac.init(spec);
		} catch (InvalidKeyException e) {
			throw new NoSuitableVisitorException(e);
		}
		BigInteger mod=BigInteger.valueOf(bucketNumber*MOD_MULTIPLIER);
		for(int i=0;i<bucketNumber;i++)
		{	
			boolean done=false;
			BigInteger current = BigInteger.valueOf(i);
			Set<BigInteger>identifiers=new HashSet<BigInteger>();
			do{
				byte[] enc=mac.doFinal(current.toByteArray());
				BigInteger encBig=new BigInteger(enc).mod(mod);
				if(!identifiers.contains(encBig))
				{
					cachedIndex[i]=encBig.longValue();
					done=true;
				}
				else
					current=encBig;
			}while(!done);
			
		}
		return cachedIndex;
	}
	
	private List<Bin> listBins(Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		if(mac.equals(cachedMac) && spec.equals(cachedSpec))
			return cachedBin;
		Long[]indexTable=computeIndexTable(mac,spec);

		cachedBin=new ArrayList<Bin>();
		for(int i=0;i<bucketNumber;i++)
		{
			Double l = null;
			Double h = null;
			if(i==0)
			{
				h=low;
			}
			else if(i==bucketNumber-1)
			{
				l=high;
			}
			else
			{
				l=low+difference*i;
				h=l+difference;
			}
			Bin bin=new Bin(l, h, indexTable[i]);
			cachedBin.add(bin);
		}
		return cachedBin;
	}
	
	public long getEncryptedOrdinal(Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		Long[]indexTable=computeIndexTable(mac,spec);
		return indexTable[bucketOrdinal].longValue();
	}
	
	public Long[] getEncryptedOrdinalLess(Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		Long[]indexTable=computeIndexTable(mac,spec);
		Long[]toRet=new Long[bucketOrdinal+1];
		for(int i=0;i<toRet.length;i++)
		{
			toRet[i]=indexTable[i].longValue();
		}
		return toRet;
	}
	
	public Long[] getEncryptedOrdinalNotEqual(Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		Long[]indexTable=computeIndexTable(mac,spec);
		Long[]toRet=new Long[indexTable.length-1];
		for(int i=0;i<toRet.length;i++)
		{
			if(i!=bucketOrdinal)
				toRet[i]=indexTable[i].longValue();
			else
				toRet[i]=indexTable[indexTable.length-1];
		}
		return toRet;
	}
	
	public Long[] getEncryptedOrdinalLessOrEqual(Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		return getEncryptedOrdinalLess(mac,spec);
	}
	
	public Long[] getEncryptedOrdinalGreater(Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		Long[]more=getEncryptedOrdinalGreaterOrEqual(mac,spec);
		if(!onLimit)
			return more;
		Long[] toRet=new Long[more.length-1];
		for(int i=0;i<toRet.length;i++)
		{
			toRet[i]=more[i+1];
		}
		return toRet;
	}
	
	public Long[] getEncryptedOrdinalGreaterOrEqual(Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		Long[]indexTable=computeIndexTable(mac,spec);
		Long[]toRet=new Long[bucketNumber-bucketOrdinal];
		for(int i=bucketOrdinal;i<toRet.length;i++)
		{
			toRet[i-bucketOrdinal]=indexTable[i].longValue();
		}
		return toRet;
	}
	
	private List<IdPair> getOverlappingForRule(BinningNumber other,Mac mac,SecretKeySpec spec,Relation rel) throws NoSuitableVisitorException
	{
		ArrayList<IdPair>toRet=new ArrayList<IdPair>();
		List<Bin> ownBins=listBins(mac, spec);
		List<Bin> otherBins=other.listBins(mac, spec);
		for(Bin own:ownBins)
		{
			long oid=own.getId();
			for(Bin otherbin:otherBins)
			{
				if(otherbin.overLap(own,rel))
					toRet.add(new IdPair(oid,otherbin.getId()));
			}
		}
		return toRet;
	}
	
	public List<IdPair> getOverlappingEquals(BinningNumber other,Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		return getOverlappingForRule(other,mac,spec,Relation.OVERLAP);
	}
	
	public List<IdPair> getOverlappingLess(BinningNumber other,Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		return getOverlappingForRule(other,mac,spec,Relation.LESS);
	}
	
	public List<IdPair> getOverlappingMore(BinningNumber other,Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		return IdPair.switchPairs(other.getOverlappingLess(this, mac, spec));
	}
	
	public List<IdPair> getOverlappingLessOrEqual(BinningNumber other,Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		List<IdPair> equals=getOverlappingEquals(other,mac,spec);
		List<IdPair> less=getOverlappingLess(other,mac,spec);
		return merge(less,equals);
	}
	
	public List<IdPair> getOverlappingMoreOrEqual(BinningNumber other,Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		return IdPair.switchPairs(other.getOverlappingLessOrEqual(this, mac, spec));
	}
	
	public List<IdPair> getOverlappingNotEqual(BinningNumber other,Mac mac,SecretKeySpec spec) throws NoSuitableVisitorException
	{
		List<IdPair> less=getOverlappingLess(other,mac,spec);
		List<IdPair> more=getOverlappingMore(other,mac,spec);
		return merge(less,more);
	}
	
	private List<IdPair> merge(List<IdPair> one,List<IdPair> other){
		
		for(IdPair curr:one)
		{
			if(!other.contains(curr))
				other.add(curr);
		}
		return other;
	}
	
}